import os
import re
import ast
import traceback
#from utils.gfunc import FUNCS_MAP as functions_mapping
import traceback
from core import exceptions

builtin_str = str

variable_regexp = r"\$([\w_]+)"
#variable_regexp2 = r"\$\{([\w_]+)\}"
variable_regexp2 = r'(?<=\${)\b\w+(?=})'
#function_regexp = r"\$\{([\w_]+\([\$\w\.\-/_ =,]*\))\}"
#function_regexp_compile = re.compile(r"^([\w_]+)\(([\$\w\.\-/_ =,]*)\)$")
function_regexp = r"\$\{([\w_]+\([\$'\"\w\.\-/_ =,]*\))\}"
function_regexp_compile = re.compile(r"^([\w_]+)\(([\$'\"\w\.\-/_ =,]*)\)$")

numeric_types = (int, float)
basestring = (str, bytes)

class Utils(object):

    @staticmethod
    def get_os_environ(variable_name):
        """ get value of environment variable.

        Args:
            variable_name(str): variable name

        Returns:
            value of environment variable.

        Raises:
            exceptions.EnvNotFound: If environment variable not found.

        """
        try:
            return os.environ[variable_name]
        except KeyError:
            raise exceptions.EnvNotFound(variable_name)

    @staticmethod
    def ensure_mapping_format(variables):
        """ ensure variables are in mapping format.

        Args:
            variables (list/dict): original variables

        Returns:
            dict: ensured variables in dict format

        Examples:
            >>> variables = [
                    {"a": 1},
                    {"b": 2}
                ]
            >>> print(ensure_mapping_format(variables))
                {
                    "a": 1,
                    "b": 2
                }

        """
        if isinstance(variables, list):
            variables_dict = {}
            for map_dict in variables:
                variables_dict.update(map_dict)

            return variables_dict

        elif isinstance(variables, dict):
            return variables

        else:
            raise exceptions.ParamsError("variables format error!")


utils = Utils()



"""变量/函数模板解析器"""

def parse_function(content):
    """ parse function name and args from string content.
    Args:
        content (str): string content
    Returns:
        dict: function meta dict
            {
                "func_name": "xxx",
                "args": [],
                "kwargs": {}
            }
    Examples:
        >>> parse_function("func()")
        {'func_name': 'func', 'args': [], 'kwargs': {}}
        >>> parse_function("func(5)")
        {'func_name': 'func', 'args': [5], 'kwargs': {}}
        >>> parse_function("func(1, 2)")
        {'func_name': 'func', 'args': [1, 2], 'kwargs': {}}
        >>> parse_function("func(a=1, b=2)")
        {'func_name': 'func', 'args': [], 'kwargs': {'a': 1, 'b': 2}}
        >>> parse_function("func(1, 2, a=3, b=4)")
        {'func_name': 'func', 'args': [1, 2], 'kwargs': {'a':3, 'b':4}}
    """
    matched = function_regexp_compile.match(content)
    if not matched:
        raise exceptions.FunctionNotFound("{} not found!".format(content))
        print("{} not found!".format(content))

    function_meta = {
        "func_name": matched.group(1),
        "args": [],
        "kwargs": {}
    }

    args_str = matched.group(2).strip()
    if args_str == "":
        return function_meta

    args_list = args_str.split(',')
    for arg in args_list:
        arg = arg.strip()
        if '=' in arg:
            key, value = arg.split('=')
            function_meta["kwargs"][key.strip()] = parse_string_value(value.strip())
        else:
            function_meta["args"].append(parse_string_value(arg))

    return function_meta

def parse_data(content, variables_mapping=None, functions_mapping=None, raise_if_variable_not_found=True):
    """ parse content with variables mapping
    Args:
        content (str/dict/list/numeric/bool/type): content to be parsed
        variables_mapping (dict): variables mapping.
        functions_mapping (dict): functions mapping.
        raise_if_variable_not_found (bool): if set False, exception will not raise when VariableNotFound occurred.
    Returns:
        parsed content.
    Examples:
        >>> content = {
                'request': {
                    'url': '/api/users/$uid',
                    'headers': {'token': '$token'}
                }
            }
        >>> variables_mapping = {"uid": 1000, "token": "abcdef"}
        >>> parse_data(content, variables_mapping)
            {
                'request': {
                    'url': '/api/users/1000',
                    'headers': {'token': 'abcdef'}
                }
            }
    """
    # TODO: refactor type check
    if content is None or isinstance(content, (numeric_types, bool, type)):
        return content

    if isinstance(content, (list, set, tuple)):
        return [
            parse_data(
                item,
                variables_mapping,
                functions_mapping,
                raise_if_variable_not_found
            )
            for item in content
        ]

    if isinstance(content, dict):
        parsed_content = {}
        for key, value in content.items():
            parsed_key = parse_data(
                key,
                variables_mapping,
                functions_mapping,
                raise_if_variable_not_found
            )
            parsed_value = parse_data(
                value,
                variables_mapping,
                functions_mapping,
                raise_if_variable_not_found
            )
            parsed_content[parsed_key] = parsed_value

        return parsed_content

    if isinstance(content, basestring):
        # content is in string format here
        variables_mapping = utils.ensure_mapping_format(variables_mapping or {})
        functions_mapping = functions_mapping or {}
        content = content.strip()

        try:
            # replace functions with evaluated value
            # Notice: parse_string_functions must be called before parse_string_variables
            content = parse_string_functions(
                content,
                variables_mapping,
                functions_mapping
            )
            # replace variables with binding value
            content = parse_string_variables(
                content,
                    variables_mapping,
                    functions_mapping
                )
        except Exception as err:
            print('[Error]: parse string vars/funcs error. %s' % err)
            traceback.print_exc()
    return content

def parse_string_variables(content, variables_mapping, functions_mapping):
    """ parse string content with variables mapping.
    Args:
        content (str): string content to be parsed.
        variables_mapping (dict): variables mapping.
    Returns:
        str: parsed string content.
    Examples:
        >>> content = "/api/users/$uid"
        >>> variables_mapping = {"$uid": 1000}
        >>> parse_string_variables(content, variables_mapping, {})
            "/api/users/1000"
    """
    variables_list = extract_variables(content)
    #if(variables_list):
    #    import pdb; pdb.set_trace()
    for variable_name in variables_list:
        variable_value = get_mapping_variable(variable_name, variables_mapping)

        if variable_name == "request" and isinstance(variable_value, dict) \
            and "url" in variable_value and "method" in variable_value:
            # call setup_hooks action with $request
            for key, value in variable_value.items():
                variable_value[key] = parse_data(
                    value,
                    variables_mapping,
                    functions_mapping
                )
            parsed_variable_value = variable_value
        elif "${}".format(variable_name) == variable_value:
            # variable_name = "token"
            # variables_mapping = {"token": "$token"}
            parsed_variable_value = variable_value
        elif "${%s}" % variable_name == variable_value:
            # variable_name = "token"
            # variables_mapping = {"token": "$token"}
            parsed_variable_value = variable_value
        else:
            parsed_variable_value = parse_data(
                variable_value,
                variables_mapping,
                functions_mapping,
                raise_if_variable_not_found=False
            )

        # TODO: replace variable label from $var to {{var}}
        if "${}".format(variable_name) == content:
            # content is a variable
            content = parsed_variable_value
        else:
            # content contains one or several variables
            if not isinstance(parsed_variable_value, str):
                parsed_variable_value = builtin_str(parsed_variable_value)
            # by YT
            if re.findall(variable_regexp, content):
                content = content.replace(
                    "${}".format(variable_name),
                    parsed_variable_value, 1
                )
            else:
                # by YT 处理${varname} 格式
                content = content.replace(
                    '${%s}' % variable_name,
                    parsed_variable_value, 1
                )
    #if(variables_list):
    #    import pdb; pdb.set_trace()
    return content

def parse_string_functions(content, variables_mapping, functions_mapping):
    """ parse string content with functions mapping.
    Args:
        content (str): string content to be parsed.
        variables_mapping (dict): variables mapping.
        functions_mapping (dict): functions mapping.
    Returns:
        str: parsed string content.
    Examples:
        >>> content = "abc${add_one(3)}def"
        >>> functions_mapping = {"add_one": lambda x: x + 1}
        >>> parse_string_functions(content, functions_mapping)
            "abc4def"
    """
    #print('[INFO]: parse_string_functions')
    functions_list = extract_functions(content)
    for func_content in functions_list:
        function_meta = parse_function(func_content)
        func_name = function_meta["func_name"]

        args = function_meta.get("args", [])
        kwargs = function_meta.get("kwargs", {})
        args = parse_data(args, variables_mapping, functions_mapping)
        kwargs = parse_data(kwargs, variables_mapping, functions_mapping)

        if func_name in ["parameterize", "P"]:
            if len(args) != 1 or kwargs:
                raise exceptions.ParamsError("P() should only pass in one argument!")
            from httprunner import loader
            eval_value = loader.load_csv_file(args[0])
        elif func_name in ["environ", "ENV"]:
            if len(args) != 1 or kwargs:
                print("ENV() should only pass in one argument!")
                raise exceptions.ParamsError("ENV() should only pass in one argument!")

            eval_value = utils.get_os_environ(args[0])
        else:
            func = get_mapping_function(func_name, functions_mapping)
            eval_value = func(*args, **kwargs)

        func_content = "${" + func_content + "}"
        if func_content == content:
                # content is a function, e.g. "${add_one(3)}"
                content = eval_value
        else:
            # content contains one or many functions, e.g. "abc${add_one(3)}def"
            content = content.replace(
                func_content,
                str(eval_value), 1
            )

    return content


def get_mapping_function(function_name, functions_mapping):
    """ get function from functions_mapping,
        if not found, then try to check if builtin function.
    Args:
        variable_name (str): variable name
        variables_mapping (dict): variables mapping
    Returns:
        mapping function object.
    Raises:
        exceptions.FunctionNotFound: function is neither defined in debugtalk.py nor builtin.
    """
    if function_name in functions_mapping:
        return functions_mapping[function_name]

    ''' by YT
    try:
        # check if HttpRunner builtin functions
        from httprunner import loader
        built_in_functions = loader.load_builtin_functions()
        return built_in_functions[function_name]
    except KeyError:
        pass
    '''

    try:
        # check if Python builtin functions
        item_func = eval(function_name)
        if callable(item_func):
            # is builtin function
            return item_func
    except (NameError, TypeError):
        # is not builtin function
        print("{} is not found!".format(function_name))
        raise exceptions.FunctionNotFound("{} is not found.".format(function_name))



def extract_functions(content):
    """ extract all functions from string content, which are in format ${fun()}
    Args:
        content (str): string content
    Returns:
        list: functions list extracted from string content
    Examples:
        >>> extract_functions("${func(5)}")
        ["func(5)"]
        >>> extract_functions("${func(a=1, b=2)}")
        ["func(a=1, b=2)"]
        >>> extract_functions("/api/1000?_t=${get_timestamp()}")
        ["get_timestamp()"]
        >>> extract_functions("/api/${add(1, 2)}")
        ["add(1, 2)"]
        >>> extract_functions("/api/${add(1, 2)}?_t=${get_timestamp()}")
        ["add(1, 2)", "get_timestamp()"]
    """
    try:
        return re.findall(function_regexp, content)
    except TypeError:
        return []


def extract_variables(content):
    """ extract all variable names from content, which is in format $variable
    Args:
        content (str): string content
    Returns:
        list: variables list extracted from string content
    Examples:
        >>> extract_variables("$variable")
        ["variable"]
        >>> extract_variables("/blog/$postid")
        ["postid"]
        >>> extract_variables("/$var1/$var2")
        ["var1", "var2"]
        >>> extract_variables("abc")
        []
    """
    # TODO: change variable notation from $var to {{var}}
    #print('extract_variables')
    try:
        vars1 = re.findall(variable_regexp, content)
        vars2 = re.findall(variable_regexp2, content)
        vars = vars1 + vars2
        return vars
    except TypeError:
        return []



def get_mapping_variable(variable_name, variables_mapping):

    """ get variable from variables_mapping.
    Args:
        variable_name (str): variable name
        variables_mapping (dict): variables mapping
    Returns:
        mapping variable value.
    Raises:
        exceptions.VariableNotFound: variable is not found.
    """
    #print('get_mapping_variable','debug')
    try:
        return variables_mapping[variable_name]
    except Exception as err:
        print('不存在该变量：%s' % err, 'error')
        traceback.print_exc()


def parse_string_value(str_value):
    """ parse string to number if possible
    e.g. "123" => 123
         "12.2" => 12.3
         "abc" => "abc"
         "$var" => "$var"
    """
    try:
        return ast.literal_eval(str_value)
    except ValueError:
        return str_value
    except SyntaxError:
        # e.g. $var, ${func}
        return str_value

if __name__ == '__main__':
    ps = Parser()

    # usage
    def get_sum(*args):
        res = 0
        for i in args:
            res += i
        return res

    def get_lang(name):
        dt = {'Silly': 'python'}
        return dt.get(name)

    def get_info(data):
        info = ''
        for k in data:
            #print('%s: %s' % (k, data[k]))
            pair = '%s: %s' % (k, data[k])
            info = "%s%s, " % (info, pair)
        return info[0:-2]

    def test1(content):
        print('[INFO]: 参数为实际值')
        fs = ps.extract_functions(content)
        r = ps.parse_string_functions(content, {}, funs_dt)
        print(r)

    def test2(content):
        print('[INFO]: 参数为变量')
        fs = ps.extract_functions(content)
        r = ps.parse_string_functions(content, vars_dt, funs_dt)
        print(r)

    vars_dt = {
        'num': '33',
        'name': 'Silly',
        'info1': {'name': 'YT', 'height': '173'}
    }

    funs_dt = {
        'get_sum': get_sum,
        'get_lang': get_lang,
        'get_info': get_info
    }

    st = 'no is ${get_sum(11,22)}'
    st1 = "silly likes ${get_lang('Silly')}"
    st2 = "silly likes ${get_lang($name)}"
    st3 = "my info: ${get_info($info)}"
    #fs = ps.extract_functions(st2)
    #print('Func: %s" % fs)
    #ps.parse_func(st2,funs_dt)
    #r = ps.parse_string_functions(fs[0], {}, funs_dt)
    #r = ps.parse_string_functions(st1, {'name': 'Silly'}, funs_dt)
    #r = ps.parse_string_functions(st2, {}, funs_dt)
    #print(r)
    #test1(st1)
    #test2(st2)
    #test1(st)
    test2(st3)
